package slap.concurrent;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import slap.exception.UnexpectedException;
import slap.paradigm.F;
import slap.paradigm.Procedure;
import slap.paradigm.F.E3;
import slap.paradigm.F.E4;
import slap.paradigm.F.E5;
import slap.paradigm.F.Either;
import slap.paradigm.F.T3;
import slap.paradigm.F.T4;
import slap.paradigm.F.T5;
import slap.paradigm.F.Tuple;
import slap.paradigm.Procedure.Procedure1;

public class Promise<V> implements Future<V>, Procedure1<V> {

        final CountDownLatch taskLock = new CountDownLatch(1);
        boolean cancelled = false;

        public boolean cancel(boolean mayInterruptIfRunning) {
            return false;
        }

        public boolean isCancelled() {
            return false;
        }

        public boolean isDone() {
            return invoked;
        }

        public V getOrNull() {
            return result;
        }

        public V get() throws InterruptedException, ExecutionException {
            taskLock.await();
            return result;
        }

        public V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
            taskLock.await(timeout, unit);
            return result;
        }
        List<Procedure1<Promise<V>>> callbacks = new ArrayList<Procedure1<Promise<V>>>();
        boolean invoked = false;
        V result = null;

        public void invoke(V result) {
            synchronized (this) {
                if (!invoked) {
                    invoked = true;
                    this.result = result;
                    taskLock.countDown();
                } else {
                    return;
                }
            }
            for (Procedure1<Promise<V>> callback : callbacks) {
                callback.invoke(this);
            }
        }

        public void onRedeem(Procedure1<Promise<V>> callback) {
            synchronized (this) {
                if (!invoked) {
                    callbacks.add(callback);
                }
            }
            if (invoked) {
                callback.invoke(this);
            }
        }

        public static <T> Promise<List<T>> waitAll(final Promise<T>... promises) {
            return waitAll(Arrays.asList(promises));
        }

        public static <T> Promise<List<T>> waitAll(final Collection<Promise<T>> promises) {
            final CountDownLatch waitAllLock = new CountDownLatch(promises.size());
            final Promise<List<T>> result = new Promise<List<T>>() {

                @Override
                public boolean cancel(boolean mayInterruptIfRunning) {
                    boolean r = true;
                    for (Promise<T> f : promises) {
                        r = r & f.cancel(mayInterruptIfRunning);
                    }
                    return r;
                }

                @Override
                public boolean isCancelled() {
                    boolean r = true;
                    for (Promise<T> f : promises) {
                        r = r & f.isCancelled();
                    }
                    return r;
                }

                @Override
                public boolean isDone() {
                    boolean r = true;
                    for (Promise<T> f : promises) {
                        r = r & f.isDone();
                    }
                    return r;
                }

                @Override
                public List<T> get() throws InterruptedException, ExecutionException {
                    waitAllLock.await();
                    List<T> r = new ArrayList<T>();
                    for (Promise<T> f : promises) {
                        r.add(f.get());
                    }
                    return r;
                }

                @Override
                public List<T> get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException {
                    waitAllLock.await(timeout, unit);
                    return get();
                }
            };
            final Procedure1<Promise<T>> action = new Procedure1<Promise<T>>() {

                public void invoke(Promise<T> completed) {
                    waitAllLock.countDown();
                    if (waitAllLock.getCount() == 0) {
                        try {
                            result.invoke(result.get());
                        } catch (Exception e) {
                            throw new UnexpectedException(e);
                        }
                    }
                }
            };
            for (Promise<T> f : promises) {
                f.onRedeem(action);
            }
            return result;
        }

        public static <A, B> Promise<F.Tuple<A, B>> wait2(Promise<A> tA, Promise<B> tB) {
            final Promise<F.Tuple<A, B>> result = new Promise<F.Tuple<A, B>>();
            final Promise<List<Object>> t = waitAll(new Promise[]{tA, tB});
            t.onRedeem(new Procedure1<Promise<List<Object>>>() {

                public void invoke(Promise<List<Object>> completed) {
                    List<Object> values = completed.getOrNull();
                    result.invoke(new F.Tuple((A) values.get(0), (B) values.get(1)));
                }
            });
            return result;
        }

        public static <A, B, C> Promise<F.T3<A, B, C>> wait3(Promise<A> tA, Promise<B> tB, Promise<C> tC) {
            final Promise<F.T3<A, B, C>> result = new Promise<F.T3<A, B, C>>();
            final Promise<List<Object>> t = waitAll(new Promise[]{tA, tB, tC});
            t.onRedeem(new Procedure1<Promise<List<Object>>>() {

                public void invoke(Promise<List<Object>> completed) {
                    List<Object> values = completed.getOrNull();
                    result.invoke(new F.T3((A) values.get(0), (B) values.get(1), (C) values.get(2)));
                }
            });
            return result;
        }

        public static <A, B, C, D> Promise<F.T4<A, B, C, D>> wait4(Promise<A> tA, Promise<B> tB, Promise<C> tC, Promise<D> tD) {
            final Promise<F.T4<A, B, C, D>> result = new Promise<F.T4<A, B, C, D>>();
            final Promise<List<Object>> t = waitAll(new Promise[]{tA, tB, tC, tD});
            t.onRedeem(new Procedure1<Promise<List<Object>>>() {

                public void invoke(Promise<List<Object>> completed) {
                    List<Object> values = completed.getOrNull();
                    result.invoke(new F.T4((A) values.get(0), (B) values.get(1), (C) values.get(2), (D) values.get(3)));
                }
            });
            return result;
        }

        public static <A, B, C, D, E> Promise<F.T5<A, B, C, D, E>> wait5(Promise<A> tA, Promise<B> tB, Promise<C> tC, Promise<D> tD, Promise<E> tE) {
            final Promise<F.T5<A, B, C, D, E>> result = new Promise<F.T5<A, B, C, D, E>>();
            final Promise<List<Object>> t = waitAll(new Promise[]{tA, tB, tC, tD, tE});
            t.onRedeem(new Procedure1<Promise<List<Object>>>() {

                public void invoke(Promise<List<Object>> completed) {
                    List<Object> values = completed.getOrNull();
                    result.invoke(new F.T5((A) values.get(0), (B) values.get(1), (C) values.get(2), (D) values.get(3), (E) values.get(4)));
                }
            });
            return result;
        }

        private static Promise<F.Tuple<Integer, Promise<Object>>> waitEitherInternal(final Promise<?>... futures) {
            final Promise<F.Tuple<Integer, Promise<Object>>> result = new Promise<F.Tuple<Integer, Promise<Object>>>();
            for (int i = 0; i < futures.length; i++) {
                final int index = i + 1;
                ((Promise<Object>) futures[i]).onRedeem(new Procedure1<Promise<Object>>() {

                    public void invoke(Promise<Object> completed) {
                        result.invoke(new F.Tuple(index, completed));
                    }
                });
            }
            return result;
        }

        public static <A, B> Promise<F.Either<A, B>> waitEither(final Promise<A> tA, final Promise<B> tB) {
            final Promise<F.Either<A, B>> result = new Promise<F.Either<A, B>>();
            final Promise<F.Tuple<Integer, Promise<Object>>> t = waitEitherInternal(tA, tB);

            t.onRedeem(new Procedure1<Promise<F.Tuple<Integer, Promise<Object>>>>() {

                public void invoke(Promise<F.Tuple<Integer, Promise<Object>>> completed) {
                    F.Tuple<Integer, Promise<Object>> value = completed.getOrNull();
                    switch (value._1) {
                        case 1:
                            result.invoke(F.Either.<A, B>_1((A) value._2.getOrNull()));
                            break;
                        case 2:
                            result.invoke(F.Either.<A, B>_2((B) value._2.getOrNull()));
                            break;
                    }

                }
            });

            return result;
        }

        public static <A, B, C> Promise<F.E3<A, B, C>> waitEither(final Promise<A> tA, final Promise<B> tB, final Promise<C> tC) {
            final Promise<F.E3<A, B, C>> result = new Promise<F.E3<A, B, C>>();
            final Promise<F.Tuple<Integer, Promise<Object>>> t = waitEitherInternal(tA, tB, tC);

            t.onRedeem(new Procedure1<Promise<F.Tuple<Integer, Promise<Object>>>>() {

                public void invoke(Promise<F.Tuple<Integer, Promise<Object>>> completed) {
                    F.Tuple<Integer, Promise<Object>> value = completed.getOrNull();
                    switch (value._1) {
                        case 1:
                            result.invoke(F.E3.<A, B, C>_1((A) value._2.getOrNull()));
                            break;
                        case 2:
                            result.invoke(F.E3.<A, B, C>_2((B) value._2.getOrNull()));
                            break;
                        case 3:
                            result.invoke(F.E3.<A, B, C>_3((C) value._2.getOrNull()));
                            break;
                    }

                }
            });

            return result;
        }

        public static <A, B, C, D> Promise<F.E4<A, B, C, D>> waitEither(final Promise<A> tA, final Promise<B> tB, final Promise<C> tC, final Promise<D> tD) {
            final Promise<F.E4<A, B, C, D>> result = new Promise<F.E4<A, B, C, D>>();
            final Promise<F.Tuple<Integer, Promise<Object>>> t = waitEitherInternal(tA, tB, tC, tD);

            t.onRedeem(new Procedure1<Promise<F.Tuple<Integer, Promise<Object>>>>() {

                public void invoke(Promise<F.Tuple<Integer, Promise<Object>>> completed) {
                    F.Tuple<Integer, Promise<Object>> value = completed.getOrNull();
                    switch (value._1) {
                        case 1:
                            result.invoke(F.E4.<A, B, C, D>_1((A) value._2.getOrNull()));
                            break;
                        case 2:
                            result.invoke(F.E4.<A, B, C, D>_2((B) value._2.getOrNull()));
                            break;
                        case 3:
                            result.invoke(F.E4.<A, B, C, D>_3((C) value._2.getOrNull()));
                            break;
                        case 4:
                            result.invoke(F.E4.<A, B, C, D>_4((D) value._2.getOrNull()));
                            break;
                    }

                }
            });

            return result;
        }

        public static <A, B, C, D, E> Promise<F.E5<A, B, C, D, E>> waitEither(final Promise<A> tA, final Promise<B> tB, final Promise<C> tC, final Promise<D> tD, final Promise<E> tE) {
            final Promise<F.E5<A, B, C, D, E>> result = new Promise<F.E5<A, B, C, D, E>>();
            final Promise<F.Tuple<Integer, Promise<Object>>> t = waitEitherInternal(tA, tB, tC, tD, tE);

            t.onRedeem(new Procedure1<Promise<F.Tuple<Integer, Promise<Object>>>>() {

                public void invoke(Promise<F.Tuple<Integer, Promise<Object>>> completed) {
                    F.Tuple<Integer, Promise<Object>> value = completed.getOrNull();
                    switch (value._1) {
                        case 1:
                            result.invoke(F.E5.<A, B, C, D, E>_1((A) value._2.getOrNull()));
                            break;
                        case 2:
                            result.invoke(F.E5.<A, B, C, D, E>_2((B) value._2.getOrNull()));
                            break;
                        case 3:
                            result.invoke(F.E5.<A, B, C, D, E>_3((C) value._2.getOrNull()));
                            break;
                        case 4:
                            result.invoke(F.E5.<A, B, C, D, E>_4((D) value._2.getOrNull()));
                            break;
                        case 5:
                            result.invoke(F.E5.<A, B, C, D, E>_5((E) value._2.getOrNull()));
                            break;

                    }

                }
            });

            return result;
        }

        public static <T> Promise<T> waitAny(final Promise<T>... futures) {
            final Promise<T> result = new Promise<T>();

            final Procedure1<Promise<T>> action = new Procedure1<Promise<T>>() {

                public void invoke(Promise<T> completed) {
                    synchronized (this) {
                        if (result.isDone()) {
                            return;
                        }
                    }
                    result.invoke(completed.getOrNull());
                }
            };

            for (Promise<T> f : futures) {
                f.onRedeem(action);
            }

            return result;
        }
    }